package bjut.zrpc.core.optimization.proxy;

import bjut.zrpc.core.optimization.bean.ZRpcServiceBean;
import bjut.zrpc.core.optimization.enums.ZRpcConfigEnum;
import bjut.zrpc.core.optimization.enums.ZRpcResponseEnum;
import bjut.zrpc.core.optimization.event.*;
import bjut.zrpc.core.optimization.extension.ExtensionLoader;
import bjut.zrpc.core.optimization.factory.SingletonFactory;
import bjut.zrpc.core.optimization.interfaces.ServiceDiscover;
import bjut.zrpc.core.optimization.interfaces.ZRpcRequestTransport;
import bjut.zrpc.core.optimization.message.ZRpcRequestMessage;
import bjut.zrpc.core.optimization.message.ZRpcResponseMessage;
import bjut.zrpc.core.optimization.netty.NettyClient;
import bjut.zrpc.core.optimization.protect.ServicesRecord;
import bjut.zrpc.core.optimization.utils.PropertiesReadUtil;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @Author: zhuzhiming
 * @Date: 2022/7/2 13:07
 */
public class ZRpcClientCglibProxy implements ProxyFactory {


    private Class<?> target;
    private final ZRpcServiceBean zRpcServiceBean;
    private final ZRpcRequestTransport zRpcRequestTransport;
    private final ServiceDiscover serviceDiscover;
    private static final ServicesRecord servicesRecord = SingletonFactory.getInstance(ServicesRecord.class);
    private static final String MOCK_METHOD_NAME = "mockMethod";
    private boolean isFirst = true;
    private boolean isMock = false;
    private SimpleZRpcEventMulticaster simpleZRpcEventMulticaster;


    public ZRpcClientCglibProxy(Class<?> target, ZRpcRequestTransport zRpcRequestTransport, ZRpcServiceBean zRpcServiceBean) {
        this.target = target;
        this.zRpcRequestTransport = zRpcRequestTransport;
        this.zRpcServiceBean = zRpcServiceBean;
        String discover = PropertiesReadUtil.getConfig(ZRpcConfigEnum.ZRPC_SERVICE_DISCOVER.getPropertyValue()[0]);
        serviceDiscover = ExtensionLoader.getExtensionLoader(ServiceDiscover.class).getExtension(discover);
        simpleZRpcEventMulticaster = SingletonFactory.getInstance(SimpleZRpcEventMulticaster.class);
    }

    @Override
    public <T> T createProxy(Class<T> clazz) {

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target);
        enhancer.setCallback(new cglibInnerMethodInterceptor(zRpcRequestTransport, zRpcServiceBean));

        return (T) enhancer.create();
    }


    private class cglibInnerMethodInterceptor implements MethodInterceptor, ZRpcEventPublisher {

        private final ZRpcServiceBean zRpcServiceBean;
        private final ZRpcRequestTransport zRpcRequestTransport;
        private final ServiceDiscover serviceDiscover;
        private final ServicesRecord servicesRecord = SingletonFactory.getInstance(ServicesRecord.class);
        private static final String MOCK_METHOD_NAME = "mockMethod";
        private boolean isFirst = true;
        private boolean isMock = false;

        public cglibInnerMethodInterceptor(ZRpcRequestTransport zRpcRequestTransport, ZRpcServiceBean zRpcServiceBean) {
            this.zRpcRequestTransport = zRpcRequestTransport;
            this.zRpcServiceBean = zRpcServiceBean;
            String discover = PropertiesReadUtil.getConfig(ZRpcConfigEnum.ZRPC_SERVICE_DISCOVER.getPropertyValue()[0]);
            serviceDiscover = ExtensionLoader.getExtensionLoader(ServiceDiscover.class).getExtension(discover);
        }


        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            publishEvent(new RemoteInvokeBeforeEvent(this));
            Object result = remoteInvoke(method, args);
            publishEvent(new RemoteInvokeAfterEvent(this));
            return result;
        }

        private Object remoteInvoke(Method method, Object[] args) {
            ZRpcRequestMessage rpcRequestMessage = ZRpcRequestMessage.builder()
                    .requestId(UUID.randomUUID().toString())
                    .interfaceName(method.getDeclaringClass().getName())
                    .methodName(method.getName())
                    .group(zRpcServiceBean.getGroup())
                    .version(zRpcServiceBean.getVersion())
                    .parameters(args)
                    .parametersType(method.getParameterTypes())
                    .build();

            List<String> serviceList = ServicesRecord.SERVICES_MAP.get(rpcRequestMessage.getServiceInfo());
            if (!isFirst && (serviceList == null || serviceList.size() == 0)) {
                if (zRpcServiceBean.isMock()) {
                    isMock = true;
                    return doMockMethod();
                }
                throw new RuntimeException("no available service");
            }

            isFirst = false;

            ZRpcResponseMessage<Object> responseMessage = null;

            servicesRecord.checkSentinel(rpcRequestMessage.getServiceInfo(), zRpcServiceBean.getDuration());

            InetSocketAddress inetSocketAddress = serviceDiscover.lookupService(rpcRequestMessage);

            if (zRpcRequestTransport instanceof NettyClient) {
                CompletableFuture<ZRpcResponseMessage<Object>> resultFuture = (CompletableFuture<ZRpcResponseMessage<Object>>) zRpcRequestTransport.sendRpcRequest(rpcRequestMessage, inetSocketAddress);
                try {
                    responseMessage = resultFuture.get(zRpcServiceBean.getTimeout(), TimeUnit.MILLISECONDS);
                } catch (InterruptedException | ExecutionException | TimeoutException e) {

                    if (e.getClass() == TimeoutException.class) {
                        return invokeTimeout(e, resultFuture, rpcRequestMessage, zRpcServiceBean);
                    } else if (e.getClass() == ExecutionException.class) {
                        return invokeExecution(e, inetSocketAddress, rpcRequestMessage, zRpcServiceBean);
                    }

                }
            }

            checkResult(rpcRequestMessage, responseMessage);

            return responseMessage.getData();
        }

        private Object invokeExecution(Exception e, InetSocketAddress inetSocketAddress, ZRpcRequestMessage rpcRequestMessage, ZRpcServiceBean zRpcServiceBean) {

            boolean needSentinel = servicesRecord.needSentinel(rpcRequestMessage.getServiceInfo() + "-" + inetSocketAddress, zRpcServiceBean.getExceptionCount(), zRpcServiceBean.getInTime());

            if (needSentinel) {
                if (zRpcServiceBean.isMock()) {
                    return doMockMethod();
                }
                throw new RuntimeException("service is die");
            }

            return e.getMessage();
        }


        private Object invokeTimeout(Exception e, CompletableFuture<ZRpcResponseMessage<Object>> resultFuture, ZRpcRequestMessage rpcRequestMessage, ZRpcServiceBean zRpcServiceBean) {

            List<String> serviceList = ServicesRecord.SERVICES_MAP.get(rpcRequestMessage.getServiceInfo());
            int retry = zRpcServiceBean.getRetry();

            if (retry == 1) {
                if (zRpcServiceBean.isMock()) {
                    return doMockMethod();
                }
                throw new RuntimeException(e.getMessage());
            } else if (retry == -1 && serviceList.size() > 1) {
                Iterator<String> iterator = serviceList.iterator();
                while (iterator.hasNext()) {
                    return recall(e, rpcRequestMessage, zRpcServiceBean, iterator);
                }
                if (zRpcServiceBean.isMock()) {
                    return doMockMethod();
                }
                throw new RuntimeException(e.getMessage());
            } else if (retry == -1 && serviceList.size() == 1) {
                try {
                    return resultFuture.get(zRpcServiceBean.getTimeout() + zRpcServiceBean.getIncrease(), TimeUnit.MILLISECONDS).getData();
                } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                    servicesRecord.removeFirst(rpcRequestMessage.getServiceInfo());
                    if (zRpcServiceBean.isMock()) {
                        return doMockMethod();
                    }
                    throw new RuntimeException(ex.getMessage());
                }
            } else if (retry > 1) {
                return recallByCount(e, rpcRequestMessage, zRpcServiceBean, retry);
            }

            throw new IllegalArgumentException("want to retry  but the retry count not invalid");

        }

        private Object recallByCount(Exception e, ZRpcRequestMessage rpcRequestMessage, ZRpcServiceBean zRpcServiceBean, int retry) {
            Iterator<String> iterator = ServicesRecord.SERVICES_MAP.get(rpcRequestMessage.getServiceInfo()).iterator();
            while (retry > 0 && iterator.hasNext()) {
                retry--;
                CompletableFuture<ZRpcResponseMessage<Object>> resultFuture;
                String[] serviceAddress = iterator.next().split(":");
                InetSocketAddress inetSocketAddress = new InetSocketAddress(serviceAddress[0], Integer.parseInt(serviceAddress[1]));
                resultFuture = (CompletableFuture<ZRpcResponseMessage<Object>>) zRpcRequestTransport.sendRpcRequest(rpcRequestMessage, inetSocketAddress);
                try {
                    return resultFuture.get(zRpcServiceBean.getTimeout() + zRpcServiceBean.getIncrease(), TimeUnit.MILLISECONDS).getData();
                } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                    servicesRecord.addUnhealthyService(rpcRequestMessage.getServiceInfo(), inetSocketAddress, true);
                    iterator.remove();
                    return recallByCount(e, rpcRequestMessage, zRpcServiceBean, retry);
                }
            }
            if (zRpcServiceBean.isMock()) {
                return doMockMethod();
            }
            throw new RuntimeException(e.getMessage());
        }

        private Object recall(Exception e, ZRpcRequestMessage rpcRequestMessage, ZRpcServiceBean zRpcServiceBean, Iterator<String> iterator) {
            CompletableFuture<ZRpcResponseMessage<Object>> resultFuture;
            String[] serviceAddress = iterator.next().split(":");
            InetSocketAddress inetSocketAddress = new InetSocketAddress(serviceAddress[0], Integer.parseInt(serviceAddress[1]));
            resultFuture = (CompletableFuture<ZRpcResponseMessage<Object>>) zRpcRequestTransport.sendRpcRequest(rpcRequestMessage, inetSocketAddress);
            try {
                return resultFuture.get(zRpcServiceBean.getTimeout() + zRpcServiceBean.getIncrease(), TimeUnit.MILLISECONDS).getData();
            } catch (InterruptedException | ExecutionException | TimeoutException ex) {
                servicesRecord.addUnhealthyService(rpcRequestMessage.getServiceInfo(), inetSocketAddress, true);
                iterator.remove();
                return invokeTimeout(e, resultFuture, rpcRequestMessage, zRpcServiceBean);
            }

        }

        public Object doMockMethod() {

            String mockMethod = zRpcServiceBean.getMockMethod();
            try {
                Class<?> aClass = Class.forName(mockMethod);
                Object instance = aClass.newInstance();
                Method method = aClass.getMethod(MOCK_METHOD_NAME);
                return method.invoke(instance);

            } catch (Exception ex) {
                throw new RuntimeException(ex.getMessage());
            }

        }


        private void checkResult(ZRpcRequestMessage rpcRequest, ZRpcResponseMessage<Object> rpcResponse) {


            if (rpcResponse == null) {
                throw new RuntimeException("call service but rpcResponse is null");
            }
            if (!rpcRequest.getRequestId().equals(rpcResponse.getRequestId())) {
                throw new RuntimeException("request id not equal response id");
            }

            if (rpcResponse.getStatueCode() == null || !rpcResponse.getStatueCode().equals(ZRpcResponseEnum.SUCCESS.getStatueCode())) {
                throw new RuntimeException("call service fail");
            }

        }

        @Override
        public void publishEvent(ZRpcEvent event) {
            simpleZRpcEventMulticaster.multicastEvent(event);
        }
    }


}
